package per.turtle.common.datasource.interceptor;

import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import per.turtle.common.datasource.annotation.DataScope;
import per.turtle.common.security.service.TokenService;
import per.turtle.system.entity.SysRole;
import per.turtle.system.entity.SysUser;
import per.turtle.system.feign.RemoteScopeService;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

/**
 * @author turtle
 */
@Component
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class
                , BoundSql.class})})
@RequiredArgsConstructor
@Slf4j
public class MyBatisInterceptor implements Interceptor {

    /**
     * 全部数据权限
     */
    public static final Integer DATA_SCOPE_ALL = 1;

    /**
     * 角色相关部门权限
     */
    public static final Integer DATA_SCOPE_ROLE_DEPT = 2;

    /**
     * 用户当前部门权限
     */
    public static final Integer DATA_SCOPE_USER_DEPT = 3;

    /**
     * 用户当前部门及下属部门数据权限
     */
    public static final Integer DATA_SCOPE_USER_DEPT_AND_CHILD = 4;

    /**
     * 仅本人数据权限
     */
    public static final Integer DATA_SCOPE_USER_SELF = 5;

    /**
     * mybatis请求有6个参数
     */
    public static final int PARAMS_SIX = 6;

    private final TokenService tokenService;

    private final RemoteScopeService remoteScopeService;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 数据权限设置
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        BoundSql boundSql;
        //由于逻辑关系，只会进入一次
        if (args.length == PARAMS_SIX) {
            //6 个参数时
            boundSql = (BoundSql) args[5];
        } else {
            //4 个参数时
            boundSql = ms.getBoundSql(parameter);
        }
        DataScope dataScope = getDataScope(ms);
        if (Objects.nonNull(dataScope)) {
            SysUser user = tokenService.getLoginUser().getSysUser();
            String origSql = boundSql.getSql();
            log.info("origSql : {}", origSql);

            // 组装新的 sql
            StringBuilder sqlString = new StringBuilder();
            for (SysRole role : user.getRoles()) {
                int rds = role.getDataScope();
                if (DATA_SCOPE_ALL.equals(rds)) {
                    sqlString = new StringBuilder();
                    break;
                } else if (DATA_SCOPE_ROLE_DEPT.equals(rds)) {
                    // select dept_id from sys_role_dept where role_id = {}
                    List<String> deptIds = remoteScopeService.getDeptIdsByRole(role.getRoleId()).getData();
                    StringBuilder inStr = new StringBuilder();
                    deptIds.forEach(deptId -> inStr.append("'").append(deptId).append("',"));
                    sqlString.append(StrUtil.format(" OR {} IN ( {} ) ", dataScope.deptFile(), inStr.deleteCharAt(inStr.length() - 1)));
                } else if (DATA_SCOPE_USER_DEPT.equals(rds)) {
                    sqlString.append(StrUtil.format(" OR {} = '{}' ", dataScope.deptFile(), user.getDeptId()));
                } else if (DATA_SCOPE_USER_DEPT_AND_CHILD.equals(rds)) {
                    // SELECT dept_id FROM sys_dept WHERE dept_id = '{}' or find_in_set( '{}' , ancestors )
                    List<String> deptIds = remoteScopeService.getDeptIdsWithChildByUser(user.getDeptId()).getData();
                    StringBuilder inStr = new StringBuilder();
                    deptIds.forEach(deptId -> inStr.append("'").append(deptId).append("',"));
                    sqlString.append(StrUtil.format(" OR {} IN ( {} ) ", dataScope.deptFile(), inStr.deleteCharAt(inStr.length() - 1)));
                } else if (DATA_SCOPE_USER_SELF.equals(rds)) {
                    sqlString.append(StrUtil.format(" OR {} = '{}' ", dataScope.userFile(), user.getUserId()));
                }
            }
            String newSql = origSql + (StrUtil.isNotBlank(sqlString.toString()) ? " AND (" + sqlString.substring(4) + ")" : "");

            // 重新new一个查询语句对象
            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(), boundSql.getParameterObject());

            // 把新的查询放到statement里
            MappedStatement newMs = newMappedStatement(ms, new BoundSqlSource(newBoundSql));
            for (ParameterMapping mapping : boundSql.getParameterMappings()) {
                String prop = mapping.getProperty();
                if (boundSql.hasAdditionalParameter(prop)) {
                    newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
                }
            }

            args[0] = newMs;
            if (args.length == PARAMS_SIX) {
                args[5] = newMs.getBoundSql(parameter);
            }
            log.info("Mapper方法：{} \nmybatis intercept sql:\n{}", ms.getId(), newSql.replaceAll("\n\\S*\n", " "));
        }

        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {
        // 可以用于分页，但目前使用pageHelper
        String pageNum = properties.getProperty("pageNum");
        log.info("mybatis intercept: >>>>>>> pageNum = {}", pageNum);
    }

    private DataScope getDataScope(MappedStatement mappedStatement) {
        String id = mappedStatement.getId();
        // 获取 Class Method
        String clazzName = id.substring(0, id.lastIndexOf('.'));
        String mapperMethod = id.substring(id.lastIndexOf('.') + 1);

        Class<?> clazz;
        try {
            clazz = Class.forName(clazzName);
        } catch (ClassNotFoundException e) {
            return null;
        }
        Method[] methods = clazz.getMethods();

        DataScope dataScope = null;
        for (Method method : methods) {
            if (method.getName().equals(mapperMethod)) {
                dataScope = method.getAnnotation(DataScope.class);
                break;
            }
        }
        return dataScope;
    }

    private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    /**
     * 定义一个内部辅助类，作用是包装 SQL
     */
    static class BoundSqlSource implements SqlSource {

        private final BoundSql boundSql;

        public BoundSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }

    }

}
